import { assign } from "min-dash";

import { is } from "../util/ModelUtil";

import { isLabelExternal, getExternalLabelBounds } from "../util/LabelUtil";

import { getMid } from "diagram-js/lib/layout/LayoutUtil";

import { isExpanded } from "../util/DiUtil";

import { getLabel } from "../modules/label-editing/LabelUtil";

import { elementToString } from "./Util";

function elementData(semantic, attrs) {
  return assign(
    {
      id: semantic.id,
      type: semantic.$type,
      businessObject: semantic
    },
    attrs
  );
}

function getWaypoints(bo, source, target) {
  var waypoints = bo.di.waypoint;

  if (!waypoints || waypoints.length < 2) {
    return [getMid(source), getMid(target)];
  }

  return waypoints.map(function(p) {
    return { x: p.x, y: p.y };
  });
}

function notYetDrawn(translate, semantic, refSemantic, property) {
  return new Error(
    translate(
      "element {element} referenced by {referenced}#{property} not yet drawn",
      {
        element: elementToString(refSemantic),
        referenced: elementToString(semantic),
        property: property
      }
    )
  );
}

/**
 * An importer that adds bpmn elements to the canvas
 *
 * @param {EventBus} eventBus
 * @param {Canvas} canvas
 * @param {ElementFactory} elementFactory
 * @param {ElementRegistry} elementRegistry
 * @param {Function} translate
 * @param {TextRenderer} textRenderer
 */
export default function BpmnImporter(
  eventBus,
  canvas,
  elementFactory,
  elementRegistry,
  translate,
  textRenderer
) {
  this._eventBus = eventBus;
  this._canvas = canvas;
  this._elementFactory = elementFactory;
  this._elementRegistry = elementRegistry;
  this._translate = translate;
  this._textRenderer = textRenderer;
}

BpmnImporter.$inject = [
  "eventBus",
  "canvas",
  "elementFactory",
  "elementRegistry",
  "translate",
  "textRenderer"
];

/**
 * Add bpmn element (semantic) to the canvas onto the
 * specified parent shape.
 */
BpmnImporter.prototype.add = function(semantic, parentElement) {
  var di = semantic.di,
    element,
    translate = this._translate,
    hidden;

  var parentIndex;

  // ROOT ELEMENT
  // handle the special case that we deal with a
  // invisible root element (process or collaboration)
  if (is(di, "bpmndi:BPMNPlane")) {
    // add a virtual element (not being drawn)
    element = this._elementFactory.createRoot(elementData(semantic));

    this._canvas.setRootElement(element);
  }

  // SHAPE
  else if (is(di, "bpmndi:BPMNShape")) {
    var collapsed = !isExpanded(semantic),
      isFrame = isFrameElement(semantic);
    hidden = parentElement && (parentElement.hidden || parentElement.collapsed);

    var bounds = semantic.di.bounds;

    element = this._elementFactory.createShape(
      elementData(semantic, {
        collapsed: collapsed,
        hidden: hidden,
        x: Math.round(bounds.x),
        y: Math.round(bounds.y),
        width: Math.round(bounds.width),
        height: Math.round(bounds.height),
        isFrame: isFrame
      })
    );

    if (is(semantic, "bpmn:BoundaryEvent")) {
      this._attachBoundary(semantic, element);
    }

    // insert lanes behind other flow nodes (cf. #727)
    if (is(semantic, "bpmn:Lane")) {
      parentIndex = 0;
    }

    if (is(semantic, "bpmn:DataStoreReference")) {
      // check whether data store is inside our outside of its semantic parent
      if (!isPointInsideBBox(parentElement, getMid(bounds))) {
        parentElement = this._canvas.getRootElement();
      }
    }

    this._canvas.addShape(element, parentElement, parentIndex);
  }

  // CONNECTION
  else if (is(di, "bpmndi:BPMNEdge")) {
    var source = this._getSource(semantic),
      target = this._getTarget(semantic);

    hidden = parentElement && (parentElement.hidden || parentElement.collapsed);

    element = this._elementFactory.createConnection(
      elementData(semantic, {
        hidden: hidden,
        source: source,
        target: target,
        waypoints: getWaypoints(semantic, source, target)
      })
    );

    if (is(semantic, "bpmn:DataAssociation")) {
      // render always on top; this ensures DataAssociations
      // are rendered correctly across different "hacks" people
      // love to model such as cross participant / sub process
      // associations
      parentElement = null;
    }

    // insert sequence flows behind other flow nodes (cf. #727)
    if (is(semantic, "bpmn:SequenceFlow")) {
      parentIndex = 0;
    }

    this._canvas.addConnection(element, parentElement, parentIndex);
  } else {
    throw new Error(
      translate("unknown di {di} for element {semantic}", {
        di: elementToString(di),
        semantic: elementToString(semantic)
      })
    );
  }

  // (optional) LABEL
  if (isLabelExternal(semantic) && getLabel(element)) {
    this.addLabel(semantic, element);
  }

  this._eventBus.fire("bpmnElement.added", { element: element });

  return element;
};

/**
 * Attach the boundary element to the given host
 *
 * @param {ModdleElement} boundarySemantic
 * @param {djs.model.Base} boundaryElement
 */
BpmnImporter.prototype._attachBoundary = function(
  boundarySemantic,
  boundaryElement
) {
  var translate = this._translate;
  var hostSemantic = boundarySemantic.attachedToRef;

  if (!hostSemantic) {
    throw new Error(
      translate("missing {semantic}#attachedToRef", {
        semantic: elementToString(boundarySemantic)
      })
    );
  }

  var host = this._elementRegistry.get(hostSemantic.id),
    attachers = host && host.attachers;

  if (!host) {
    throw notYetDrawn(
      translate,
      boundarySemantic,
      hostSemantic,
      "attachedToRef"
    );
  }

  // wire element.host <> host.attachers
  boundaryElement.host = host;

  if (!attachers) {
    host.attachers = attachers = [];
  }

  if (attachers.indexOf(boundaryElement) === -1) {
    attachers.push(boundaryElement);
  }
};

/**
 * add label for an element
 */
BpmnImporter.prototype.addLabel = function(semantic, element) {
  var bounds, text, label;

  bounds = getExternalLabelBounds(semantic, element);

  text = getLabel(element);

  if (text) {
    // get corrected bounds from actual layouted text
    bounds = this._textRenderer.getExternalLabelBounds(bounds, text);
  }

  label = this._elementFactory.createLabel(
    elementData(semantic, {
      id: semantic.id + "_label",
      labelTarget: element,
      type: "label",
      hidden: element.hidden || !getLabel(element),
      x: Math.round(bounds.x),
      y: Math.round(bounds.y),
      width: Math.round(bounds.width),
      height: Math.round(bounds.height)
    })
  );

  return this._canvas.addShape(label, element.parent);
};

/**
 * Return the drawn connection end based on the given side.
 *
 * @throws {Error} if the end is not yet drawn
 */
BpmnImporter.prototype._getEnd = function(semantic, side) {
  var element,
    refSemantic,
    type = semantic.$type,
    translate = this._translate;

  refSemantic = semantic[side + "Ref"];

  // handle mysterious isMany DataAssociation#sourceRef
  if (side === "source" && type === "bpmn:DataInputAssociation") {
    refSemantic = refSemantic && refSemantic[0];
  }

  // fix source / target for DataInputAssociation / DataOutputAssociation
  if (
    (side === "source" && type === "bpmn:DataOutputAssociation") ||
    (side === "target" && type === "bpmn:DataInputAssociation")
  ) {
    refSemantic = semantic.$parent;
  }

  element = refSemantic && this._getElement(refSemantic);

  if (element) {
    return element;
  }

  if (refSemantic) {
    throw notYetDrawn(translate, semantic, refSemantic, side + "Ref");
  } else {
    throw new Error(
      translate("{semantic}#{side} Ref not specified", {
        semantic: elementToString(semantic),
        side: side
      })
    );
  }
};

BpmnImporter.prototype._getSource = function(semantic) {
  return this._getEnd(semantic, "source");
};

BpmnImporter.prototype._getTarget = function(semantic) {
  return this._getEnd(semantic, "target");
};

BpmnImporter.prototype._getElement = function(semantic) {
  return this._elementRegistry.get(semantic.id);
};

// helpers ////////////////////

function isPointInsideBBox(bbox, point) {
  var x = point.x,
    y = point.y;

  return (
    x >= bbox.x &&
    x <= bbox.x + bbox.width &&
    y >= bbox.y &&
    y <= bbox.y + bbox.height
  );
}

function isFrameElement(semantic) {
  return is(semantic, "bpmn:Group");
}
